'use client'; import './style.scss'; import { usePathname, useSearchParams } from 'next/navigation'; import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { BoardLayout, BoardSort, PostSearchType } from '@/constants/forum'; import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { fetchApi } from '@/lib/utils/client'; import useErrorAlert from '@/hooks/useErrorAlert'; import Loading from '@/app/component/Loading'; import Pagination from '@/app/component/Pagination'; import { BoardResponse, BoardPostsResponse } from '@/types/response/forum/board'; import Post from '@/types/forum/post'; import useDragScroll from '@/hooks/useDragScroll'; import PostWriteButton from '../_component/PostWriteButton'; import NavTab from '@/app/(main)/support/navTab'; import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import HeaderContent from '../_component/HeaderContent'; import FooterContent from '../_component/FooterContent'; import DefaultListLayout from '../_component/DefaultListLayout'; import AlbumListLayout from '../_component/AlbumListLayout'; import QnAListLayout from '../_component/QnAListLayout'; type ViewProps = { _query: { page: number; perPage: number; prefix?: number; sort?: BoardSort; search: PostSearchType; keyword?: string; }, _board: BoardResponse, _postList: BoardPostsResponse }; export default function View({ _query, _board, _postList }: ViewProps) { const pathname = usePathname(); const searchParams = useSearchParams(); const { setError } = useErrorAlert(); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(_postList.total); const [speaker, setSpeaker] = useState(_postList.speaker); const [notice, setNotice] = useState(_postList.notice); const [list, setList] = useState(_postList.list); const [page, setPage] = useState(_query.page); const [perPage, setPerPage] = useState(_query.perPage); const [boardPrefixID, setBoardPrefixID] = useState(_query.prefix); const [sort, setSort] = useState(_query.sort); const [search, setSearch] = useState(_query.search); const [keyword, setKeyword] = useState(_query.keyword); const [params, setParams] = useState>({}); const [searchDialogOpen, setSearchDialogOpen] = useState(false); const isMounted = useRef(false); const searchRef = useRef(search); const keywordRef = useRef(keyword); const dragScroll = useDragScroll(); searchRef.current = search; keywordRef.current = keyword; const startIndex = useMemo(() => total - ((page - 1) * perPage), [total, page, perPage]); // 상태 => URL 동기화 useEffect(() => { // 기존 URL 파라미터 const alreadyParams = new URLSearchParams(searchParams.toString()); // URL 파라미터 덮어쓰기 및 삭제 Object.entries(params).forEach(([k, v]) => { if (v) { alreadyParams.set(k, v); } else { alreadyParams.delete(k); } }); const queryString = `?${alreadyParams.toString()}`; if (window.location.search !== queryString) { window.history.replaceState(null, '', `${pathname}${queryString}`); } }, [page, perPage, boardPrefixID, sort, search, keyword, params, pathname, searchParams]); const handleFetchPosts = useCallback(async () => { try { setLoading(true); const queryParams = new URLSearchParams(); queryParams.set('boardID', String(_board.id)); queryParams.set('page', String(page)); queryParams.set('perPage', String(perPage)); if (boardPrefixID) { queryParams.set('boardPrefixID', String(boardPrefixID)); } if (sort !== undefined && sort !== null) { queryParams.set('sort', String(sort)); } if (searchRef.current !== undefined && searchRef.current !== null) { queryParams.set('search', String(searchRef.current)); } if (keywordRef.current) { queryParams.set('keyword', keywordRef.current); } const res = await fetchApi(`/api/forum/posts?${queryParams.toString()}`); if (!res.data) { setError('게시글을 불러올 수 없습니다.'); } else { setTotal(res.data.total); setSpeaker(res.data.speaker); setNotice(res.data.notice); setList(res.data.list); } } catch (err) { if (err instanceof Error) { setError(err.message || '알 수 없는 오류가 발생했습니다.'); } } finally { setLoading(false); } }, [_board.id, boardPrefixID, page, perPage, sort]); const handlePageChange = useCallback((page: number) => { setPage(page); setParams((prev) => ({ ...prev, page: String(page) })); }, []); const handleChange = useCallback((e: React.ChangeEvent) => { const { name, value } = e.target; let key = ''; switch (name) { case 'boardPrefixID': setBoardPrefixID((Number(value) || undefined) as number); key = 'prefix'; break; case 'sort': setSort(Number(value) as BoardSort); break; case 'perPage': setPerPage(Number(value)); break; case 'search': setSearch(Number(value) as PostSearchType); break; case 'keyword': setKeyword(value); break; } if (['sort', 'perPage', 'search', 'keyword'].includes(name)) { key = name; } if (['boardPrefixID', 'perPage', 'search', 'keyword'].includes(name)) { handlePageChange(1); } setParams((prev) => ({ ...prev, [key]: value })); }, [handlePageChange]); const handleSearch = useCallback((e: React.FormEvent) => { e.preventDefault(); handleFetchPosts(); }, [handleFetchPosts]); const handleSearchDialog = useCallback((e: React.FormEvent) => { e.preventDefault(); handleFetchPosts(); setSearchDialogOpen(false); }, [handleFetchPosts]); useEffect(() => { if (!isMounted.current) { isMounted.current = true; return; } handleFetchPosts(); }, [page, perPage, boardPrefixID, sort, handleFetchPosts]); return ( <> {_board.code === 'notice' && }
{loading && }
{/* 말머리 */}

{ _board.name }

  • {_board.boardPrefix.map((row, i) => (
  • ))}
{/* 정렬 */}
{/* 출력 수 */}
{/* 게시글 목록 */} {(() => { switch (_board.boardMeta.list.layout) { case BoardLayout.Media: return ; case BoardLayout.QnA: return ; default: return ; } })()} {/* 검색 */}
{/* 모바일: 검색 아이콘 → Dialog */} 게시글 검색
{/* 데스크톱: 인라인 검색 폼 */}
{/* 글쓰기 버튼 */}
); }